﻿using System;
using System.IO;
using System.Text;
using System.Collections.Generic;

using log4net;

using Poi.NET.poifs.common;
using Poi.NET.poifs.filesystem;
using Poi.NET.util;

namespace Poi.NET.poifs.storage
{
    /**
     * The block containing the archive header
     *
     * @author Marc Johnson (adapted by Josh Fornwall)
     */

    public readonly class HeaderBlockReader
    {
	/**
	 * What big block size the file uses. Most files
	 *  use 512 bytes, but a few use 4096
	 */
	private readonly int bigBlockSize;

	/** number of big block allocation table blocks (int) */
	private readonly int _bat_count;

	/** start of the property set block (int index of the property set
	 * chain's first big block)
	 */
	private readonly int _property_start;

	/** start of the small block allocation table (int index of small
	 * block allocation table's first big block)
	 */
	private readonly int _sbat_start;

	/** big block index for extension to the big block allocation table */
	private readonly int _xbat_start;
	private readonly int _xbat_count;
	private readonly byte[] _data;

	/**
	 * create a new HeaderBlockReader from an InputStream
	 *
	 * @param stream the source InputStream
	 *
	 * @exception IOException on errors or bad data
	 */
	public HeaderBlockReader(Stream stream) {
		// At this point, we don't know how big our
		//  block sizes are
		// So, read the first 32 bytes to check, then
		//  read the rest of the block
		byte[] blockStart = new byte[32];
		int bsCount = IOUtils.readFully(stream, blockStart);
		if(bsCount != 32) {
			throw alertShortRead(bsCount, 32);
		}

		// verify signature
		ulong signature = LittleEndian.getLong(blockStart, HeaderBlockConstants._signature_offset);

		if (signature != HeaderBlockConstants._signature) {
			// Is it one of the usual suspects?
			byte[] OOXML_FILE_HEADER = POIFSConstants.OOXML_FILE_HEADER;
			if(blockStart[0] == OOXML_FILE_HEADER[0] &&
				blockStart[1] == OOXML_FILE_HEADER[1] &&
				blockStart[2] == OOXML_FILE_HEADER[2] &&
				blockStart[3] == OOXML_FILE_HEADER[3]) {
				throw new OfficeXmlFileException("The supplied data appears to be in the Office 2007+ XML. You are calling the part of POI that deals with OLE2 Office Documents. You need to call a different part of POI to process this data (eg XSSF instead of HSSF)");
			}
			if ((signature & 0xFF8FFFFFFFFFFFFFL) == 0x0010000200040009L) {
				// BIFF2 raw stream starts with BOF (sid=0x0009, size=0x0004, data=0x00t0)
				throw new ArgumentException("The supplied data appears to be in BIFF2 format.  "
						+ "POI only supports BIFF8 format");
			}

			// Give a generic error
			throw new IOException("Invalid header signature; read "
				                  + longToHex(signature) + ", expected "
				                  + longToHex(HeaderBlockConstants._signature));
		}


		// Figure out our block size
		switch (blockStart[30]) {
			case 12:
				bigBlockSize = POIFSConstants.LARGER_BIG_BLOCK_SIZE; break;
			case  9:
				bigBlockSize = POIFSConstants.BIG_BLOCK_SIZE; break;
			default:
				throw new IOException("Unsupported blocksize  (2^"
						+ blockStart[30] + "). Expected 2^9 or 2^12.");
		}
		_data = new byte[ bigBlockSize ];
		Array.Copy(blockStart, 0, _data, 0, blockStart.Length);

		// Now we can read the rest of our header
		int byte_count = IOUtils.readFully(stream, _data, blockStart.Length, _data.Length - blockStart.Length);
		if (byte_count+bsCount != bigBlockSize) {
			throw alertShortRead(byte_count, bigBlockSize);
		}

		_bat_count      = getInt(HeaderBlockConstants._bat_count_offset, _data);
		_property_start = getInt(HeaderBlockConstants._property_start_offset, _data);
		_sbat_start     = getInt(HeaderBlockConstants._sbat_start_offset, _data);
		_xbat_start     = getInt(HeaderBlockConstants._xbat_start_offset, _data);
		_xbat_count     = getInt(HeaderBlockConstants._xbat_count_offset, _data);
	}

	private static int getInt(int offset, byte[] data) {
		return LittleEndian.getInt(data, offset);
	}

	private static String longToHex(ulong value) {
		return new String(HexDump.longToHex(value));
	}

	private static IOException alertShortRead(int pRead, int expectedReadSize) {
		int read;
		if (pRead < 0) {
			//Can't have -1 bytes read in the error message!
			read = 0;
		} else {
			read = pRead;
		}
		String type = " byte" + (read == 1 ? (""): ("s"));

		return new IOException("Unable to read entire header; "
				+ read + type + " read; expected "
				+ expectedReadSize + " bytes");
	}

	/**
	 * get start of Property Table
	 *
	 * @return the index of the first block of the Property Table
	 */
	public int getPropertyStart() {
		return _property_start;
	}

	/**
	 * @return start of small block allocation table
	 */
	public int getSBATStart() {
		return _sbat_start;
	}

	/**
	 * @return number of BAT blocks
	 */
	public int getBATCount() {
		return _bat_count;
	}

	/**
	 * @return BAT array
	 */
	public int[] getBATArray() {
		int[] result = new int[ HeaderBlockConstants._max_bats_in_header ];
		int   offset = HeaderBlockConstants._bat_array_offset;

		for (int j = 0; j < HeaderBlockConstants._max_bats_in_header; j++) {
			result[ j ] = LittleEndian.getInt(_data, offset);
			offset      += LittleEndianConsts.INT_SIZE;
		}
		return result;
	}

	/**
	 * @return XBAT count
	 */
	public int getXBATCount() {
		return _xbat_count;
	}

	/**
	 * @return XBAT index
	 */
	public int getXBATIndex() {
		return _xbat_start;
	}

	/**
	 * @return The Big Block size, normally 512 bytes, sometimes 4096 bytes
	 */
	public int getBigBlockSize() {
		return bigBlockSize;
	}    }
}
